Repository navigation
Agents: the workflow tool sends then attaches, so a crash no longer reports a run as failed; the CLI shows steered messages - #306
Conversation
… run it started A session object runs again from its first line after a crash, so it calls again what the crashed attempt called. An idempotency key makes that the same call, except for a workflow's main handler: Restate takes no idempotency key for it, and refuses the second call (409, "the workflow method was already invoked"). The agents' `workflow` tool, and LangGraph's `nodes.workflow`, then told the model that the workflow failed, while it ran to the end. A call to a workflow's main handler (`ctx.call(wf.main, ..., key=id)`) that is refused this way now attaches to the workflow's run by its id, through the shared core's `AttachInvocationTarget::WorkflowId` (`attach_workflow` on the VM and the server context), and returns its result: the run of that id, whoever started it. Any other 409 (a cancellation), a workflow's shared handlers, and calls by name are unchanged. Tests: the rejoin and what isn't rejoined (fake host), the agents' workflow tool after a crash, and a session handler that crashes once its workflow call is made, against a real server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…here the turn took it After the conversation so far, the terminal showed none of the user's messages, since it took them to be typed here. So a message sent with `/steer` never showed where the turn took it (between a tool's result and the next step), and messages from other clients didn't show at all. The terminal now notes the messages it sends to `chat`, and hides only those (once each, by text); every other message of the user shows in its place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues. One small edge case in the CLI, noted inline.
Reviewed changes
I reviewed both commits: a session call made again to a workflow's main handler now attaches to the run instead of failing with 409, and the agents CLI now shows user messages that weren't typed in this terminal.
attach_workflowbinding:PyVM.attach_workflowwraps shared-core'sAttachInvocationTarget::WorkflowIdwithscope: None. The field matches the vendored 8.4 crate.VMWrapper,ServerInvocationContextand the session host pass it through.- Rejoin in
CallFuture:ctx.callon a handler withkind == "workflow"gets arejoin. If the call fails with a 409 whose message contains "already invoked", it awaits the attach. Cancellation 409s, shared handlers andgeneric_callby name keep their current behaviour. - Agents/LangGraph docstrings: these now say a workflow runs once per id, and that a repeated
keygets the earlier run's result. - CLI
Terminal: texts sent tochatare recorded withsending/unsent._typedhides one user message per recorded text and shows every other user message, so steered messages and messages from other clients now appear. - Tests: the fake host gains
refusalsandattached; there is an in-process rejoin test, an agents tool test, a Docker harness test on both stores, and a newtests/agents_cli.py.
claude-opus-5-5 | 𝕏
A harness test of the new API: a workflow returns "hi", a service sends it a run (ignoring the result), attaches to it with `attach_workflow`, and answers with what the attach returned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…up its text there The first change (the conversation so far) shows every message, so it never used up the text of a line typed before it came. A later user message of the same text (steered, or from another client) was then hidden in its place. The first change now uses it up too, while still showing the message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. The CLI edge case from the last review is fixed.
Reviewed changes
Since the last review, one commit changed the agents CLI and added a test for it.
- History change uses up typed text:
Terminal.shownow calls_typedfor every user message, including those in the first change (the conversation so far). It still shows them there. So text typed before that change arrives no longer stays inself.sentand hides a later message with the same text, such as a/steermessage or one from another client. - Test:
tests/agents_cli.pyadds a case that typesHibefore the history arrives, then checks that a laterHifrom another client is shown.
claude-opus-5-5 | 𝕏
…agents' tools rather than in ctx.call The rejoin moves up, out of restate.sessions' `ctx.call`, which is back to what 1.5.0 does: a call to a workflow's main handler that Restate refuses is that error again, for every session object. The agents' `workflow` tool and LangGraph's `nodes.workflow` now run the workflow through `run_workflow`: they call its main handler, and if Restate refuses the call because the run of that id started (409, "already invoked": the tool call run again after a crash), they wait for that run with `attach_workflow` and return its result. A first call stays a call of the attempt, so stopping the turn still cancels it. `restate.sessions.internal.attach_workflow(ctx, name, key)` is how a library reaches the server context's `attach_workflow` from a session object's context. Tests: the helper (fake host); the workflow tool rejoins on "already invoked" and not on another 409; against a real server, a workflow tool call run again after a crash that followed its call answers with the run's result (it answers "Error: the workflow method was already invoked" without the rejoin). The sessions-level rejoin tests are gone with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. The workflow rejoin now lives in the agents' tools, and
restate.sessions'ctx.callis back to its 1.5.0 behaviour.
Reviewed changes
Since the last review, one commit moved the rejoin for "already invoked" workflow calls out of restate.sessions and into the agents' tools.
- Reverted
sessionscall rejoin:_calls.pyno longer hasrejoinoralready_invoked._targetandCallFutureare back to their original form, and the_api.pydocstring and thesessions_harness.pyrejoin test are gone.CallHostkeepsattach_workflow, now typedAwaitable[bytes]. - Added
sessions.internal.attach_workflow: it accepts only an attempt or view context, raisesTypeErrorfor any other context, and delegates to the call host. - Added
run_workflowinagents/_tools.py: it calls the main handler as before. When Restate refuses the call with a 409 whose message says "already invoked", it attaches to the run by its workflow id and deserializes the result with the handler's output serde. Theworkflowtool and LangGraph'snodes.workflowboth use it. Any other 409 is raised again unchanged. - Tests:
tests/agents_tools.pycovers both the rejoin and the non-rejoin 409 case. The newtests/agents_tools_server.pycovers a crash after the run started, against a real Restate. Insessions_object.py, the rejoin tests are replaced by a test ofinternal.attach_workflow. Locally,agents_tools.pyandsessions_object.pypass (34 tests) and ruff is clean. The Docker-backed tests can't run on this runner.
claude-opus-5-5 | 𝕏
… send's invocation A workflow runs once per id, and Restate takes no idempotency key for its main handler. The agents' `workflow` tool, and LangGraph's `nodes.workflow`, now always send to the workflow, keep the send's invocation id, and attach to that invocation for the result (`run_workflow`). A send to a workflow whose run started already starts nothing, and its invocation id is that run's: a tool call made again after a crash, or a `key` that ran before, waits for that run. No call is refused, and no error is matched. The run is the workflow's own: stopping the turn doesn't cancel it. `restate.sessions.internal` gains `start` (a send right away that returns its invocation id) and `attach_invocation`, for libraries. The session-context `attach_workflow` helper of the previous commit is gone; the SDK's `attach_workflow` stays. Tests: against a real server, two sends to one workflow id give the same invocation id, which attach returns "hi" for; and the workflow tool run again after a crash that followed its send answers "hi". The tool and the helpers, against the fake host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tool attaches to The workflow tool gets the run's invocation id from its send, and attaches to it with `attach_invocation`, so attaching by workflow id is not needed: the binding (`PyVM.attach_workflow`), `VMWrapper.attach_workflow`, `ServerInvocationContext.attach_workflow` and its harness test go. This PR changes no Rust. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. The workflow tool now sends, then attaches to the send's invocation, so nothing is matched on error text and the native
attach_workflowbinding is gone.
Reviewed changes
Since the last review, two commits replaced the "call, then attach on 409" approach. The workflow tool now always sends to the workflow and attaches to the invocation id of that send.
- Removed
attach_workflow:src/lib.rs,vm.py,server_context.py,_ContextHostandinternalno longer have it. Without it, the PR doesn't touch the native extension. - Added
internal.startandinternal.attach_invocation:startsends right away with the newsend_tohelper and returns the send's invocation id.attach_invocationwaits through the SDK's existingattach_invocation. Both accept only an attempt context. - Simplified
run_workflow: it is nowstartfollowed byattach_invocation, and the result is deserialized with the handler's output serde.already_invokedand the idempotency key parameter are gone. Both theworkflowtool and LangGraph'snodes.workflowuse it. Stopping a turn still cuts the wait short, becauseStop.guardcancels the framework task that awaits the attach. Only the workflow run itself keeps going, which the docstring says. - Updated tests: the fake host's
generic_sendnow returns a handle whose invocation id is derived from the target. The tool test runs the tool twice and checks that this gives two sends, two attaches to the same id, and no calls. The server tests check that a second send gives the same invocation id and that the tool returns the result after a crash. Locally,agents_tools.py,sessions_object.pyandagents_cli.pypass (37 tests), ruff is clean, and pyright reports no errors onsessions/and_tools.py. The Docker-backed tests can't run on this runner.
claude-opus-5-5 | 𝕏

Two fixes found while sanity-checking modal-template against 1.5.0. Python only; no change to the native extension.
1. The workflow tool sends to the workflow, then attaches to the send's invocation
Agents are session objects. After a crash they run again from their first line, so a tool call cut off by the crash is made again. The agents' Restate tools rely on idempotency keys for that: a repeated call is the same call. A workflow's main handler is the exception. Restate takes no idempotency key for it, and refuses a second call with 409, "the workflow method was already invoked".
Before (1.4.2 and 1.5.0): if the process died while a
workflowtool's workflow ran, the retried turn told the modelError: the workflow method was already invoked, even though the workflow ran to the end. I reproduced it with modal-template'ssupportexample: kill the process during the refund. The turn records the error, whilesys_invocationshowsrefund/…/runcompleted successfully. LangGraph'snodes.workflowhas the same problem.Now: the
workflowtool andnodes.workflowalways send to the workflow, take the invocation id from the send, and attach to that invocation for the result (run_workflow).keythat ran before, waits for that run.restate.sessions.internalgainsstart(a send made right away that returns its invocation id) andattach_invocation(over the SDK'sattach_invocation), for libraries.restate.sessions'ctx.callis unchanged from 1.5.0.2. The agents CLI shows the user's messages it didn't type
Before: after the conversation so far, the terminal hid every user message, taking them as typed here. A
/steermessage never showed where the turn took it, and messages from other clients didn't show at all.Now: it notes the messages it sends to
chat, and hides only those, once each by text. That includes lines typed before the first change arrived, which pullfrog pointed out. Every other user message shows in its place:Tests
Against a real server (harness):
servercontext.py: a workflow returns "hi". A service sends to it twice: both sends give the same invocation id, and attaching to it returns "hi".agents_tools_server.py(new): theworkflowtool runs again after a crash that followed its send, and answers "hi". With 1.5.0's call instead, it answersError: the workflow method was already invoked.In-process (fake host):
sessions.internal.startandattach_invocation.agents_cli.py(new): what the terminal shows for typed, steered and others' messages.Runs:
just test-fast: 358 passed.agents_tools_server,agents_langgraph_nodes,sessions_harnessandservercontextpass. On an earlier head, the full server suite gave 256 passed and 3 failed. Those 3 registerhost.docker.internal, which doesn't resolve without Docker.🤖 Generated with Claude Code